package camid.services.nodemanager;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.UUID;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.reflections.Reflections;

import camid.distribution.activeobject.ObjectScheduler;
import camid.distribution.activeobject.ProxyBuilder;
import camid.distribution.broker.Requestor;
import camid.distribution.events.EventHandler;
import camid.distribution.lifecycle.LifecycleScope;
import camid.distribution.lifecycle.LocationScope;
import camid.distribution.lifecycle.ManagedObject;
import camid.distribution.lifecycle.ObjectManager;
import camid.distribution.protocol.Endpoint;
import camid.services.nameservice.INameService;
import camid.services.nameservice.NameService;
import camid.services.nameservice.NameServiceUtil;
import camid.services.nodemanager.observers.InvokerObserver;
import camid.services.nodemanager.observers.LifecycleObserver;
import camid.services.nodemanager.observers.SystemUsageObserver;
import camid.test.testobjects.Calculator;
import camid.test.testobjects.DelayObject;
import camid.test.testobjects.IntegerBuffer;
import camid.test.testobjects.ServerHelloMessage;
import camid.test.testobjects.StatefulDelayObject;
import camid.util.monitoring.NodeMetrics;


@ManagedObject( lifecycleScope = LifecycleScope.STATIC, locationScope = LocationScope.NODE )
public class NodeManager extends EventHandler implements INodeManager {

	public static final String NODE_MANAGER_HANDLER = "NodeManager";

	public static final double OBJECT_WEIGHT = 10.0;
	public static final double CPU_WEIGHT = 10.0;
	public static final double RESPONSE_TIME_WEIGHT = 0.001;
	
	private static final Logger logger = LogManager.getLogger(NodeManager.class.getName());
	
	private INameService nameService;
	
	private ObjectManager objectManager;
	
	private String nodeId;
	
	private Map<String, ObjectStatus> statusMap; 
	
	private Double cpuUsage;
	
	private Double memoryUsage;
	
	private Requestor requestor;
	
	public NodeManager() {
		super(NODE_MANAGER_HANDLER);
		this.cpuUsage = 0.0;
		this.memoryUsage = 0.0;
		this.statusMap = Collections.synchronizedMap(new HashMap<String, ObjectStatus> ());
		this.registerObserver(new LifecycleObserver());
		this.registerObserver(new SystemUsageObserver());
		this.registerObserver(new InvokerObserver());
		
	}
	
	public void setRequestor(Requestor requestor)
	{
		this.requestor = requestor;
	}

	public void setNameService(INameService nameService, int nodePort) 
	{
		this.nameService = nameService;
		this.nodeId = this.nameService.registerNode(NameServiceUtil.getLocalEndpoint(nodePort));
	}

	public void setObjectManager(ObjectManager objectManager) throws InstantiationException, IllegalAccessException 
	{
		this.objectManager = objectManager;
		this.objectManager.insertObject(nodeId, this);
	}

	/* (non-Javadoc)
	 * @see camid.services.nodemanager.INodeManager#getNameService()
	 */
	public INameService getNameService() {
		
		return this.nameService;
	}

	/* (non-Javadoc)
	 * @see camid.services.nodemanager.INodeManager#reportObjectCreation(java.lang.String, java.lang.Class)
	 */
	public void reportObjectCreation(String objectId, Class<?> entityClass) 
	{
		ManagedObject mo = entityClass.getAnnotation(ManagedObject.class);
		if(mo.locationScope().equals(LocationScope.CLOUD))
		{
			logger.info("Created object " + entityClass.getCanonicalName() + "/" + nodeId );
			this.nameService.bind(nodeId, objectId, entityClass.getCanonicalName());
			this.statusMap.put(objectId, ObjectStatus.ACTIVE);
		}
		
	}
	
	/* (non-Javadoc)
	 * @see camid.services.nodemanager.INodeManager#reportObjectDestruction(java.lang.String)
	 */
	public void reportObjectDestruction(String objectId) {
		this.nameService.unbind(objectId);
		this.statusMap.remove(objectId);
	}

	/* (non-Javadoc)
	 * @see camid.services.nodemanager.INodeManager#getNodeId()
	 */
	public String getNodeId() {
		
		return this.nodeId;
	}

	/* (non-Javadoc)
	 * @see camid.services.nodemanager.INodeManager#registerDefinition(java.lang.Class)
	 */
	public void registerDefinition(Class<?> entityClass) 
	{
		logger.info("Registering definition " + entityClass.getCanonicalName());
		this.nameService.registerObjectDefinition(nodeId, entityClass.getCanonicalName());
	}

	/* (non-Javadoc)
	 * @see camid.services.nodemanager.INodeManager#getEntityData(java.lang.String)
	 */
	public Object getEntityData(String objectId) 
	{
		Object entity = this.objectManager.getObject(objectId).getEntity();
		return entity;
	}

	/* (non-Javadoc)
	 * @see camid.services.nodemanager.INodeManager#addEntityData(java.lang.String, java.lang.Object)
	 */
	public void addEntityData(String objectId, Object entity) throws InstantiationException, IllegalAccessException 
	{	
		this.objectManager.insertObject(objectId, entity);
		
	}
	
	/* (non-Javadoc)
	 * @see camid.services.nodemanager.INodeManager#getCpuUsage()
	 */
	public Double getCpuUsage() 
	{
		return cpuUsage;
	}
	
	public void setCpuUsage(Double usage)
	{
		this.cpuUsage = usage;
	}

	public void setMemoryUsage(Double memory) 
	{
		this.memoryUsage = memory;	
	}
	
	public Double getMemoryUsage()
	{
		return memoryUsage;
	}
	
	/* (non-Javadoc)
	 * @see camid.services.nodemanager.INodeManager#getNumberOfObjects()
	 */
	public Integer getNumberOfObjects() 
	{
		Integer result = 0;
		synchronized (statusMap) {
			for(Entry<String,ObjectStatus> entry : statusMap.entrySet())
			{
				if(entry.getValue().equals(ObjectStatus.ACTIVE))
				{
					result++;
				}
			}
		}
		
		return result;
	}

	/* (non-Javadoc)
	 * @see camid.services.nodemanager.INodeManager#getObjectStatus(java.lang.String)
	 */
	public ObjectStatus getObjectStatus(String objectId) {
		ObjectStatus status = this.statusMap.get(objectId);
		return status;
	}

	/* (non-Javadoc)
	 * @see camid.services.nodemanager.INodeManager#migrateObject(java.lang.String, camid.services.nodemanager.NodeManager)
	 */
	public void migrateObject(String objectId, INodeManager nm) {
		if( this.statusMap.containsKey(objectId) )
		{
			try {
				this.statusMap.put(objectId, ObjectStatus.MOVING);
				ObjectScheduler scheduler = this.objectManager.getObject(objectId);
				Object entity = scheduler.getEntity();
//				if( checkEntityStatefulness(entity) )
//				{
					nm.addEntityData(objectId, entity);
//				}
				this.statusMap.put(objectId, ObjectStatus.MOVED);
				
			} catch (InstantiationException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			} catch (IllegalAccessException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
		
	}

	private boolean checkEntityStatefulness(Object entity) {
		boolean result = true;
		if( entity.getClass().isAnnotationPresent(ManagedObject.class) )
		{
			ManagedObject mo = entity.getClass().getAnnotation(ManagedObject.class);
			if(mo.lifecycleScope().equals(LifecycleScope.PER_REQUEST))
			{
				result = false;
			}
		}
		return result;
	}

	/* (non-Javadoc)
	 * @see camid.services.nodemanager.INodeManager#getNodeCost()
	 */
	public Double getNodeCost() 
	{
		Double cost = cpuUsage.doubleValue() * CPU_WEIGHT + 
				this.getNumberOfObjects().doubleValue() * OBJECT_WEIGHT + 
				this.nodeAverageResponseTime * RESPONSE_TIME_WEIGHT;
		logger.info("Cost of " + nodeId + ": " + cost);
		return cost;
	}
	
	@Override
	public NodeMetrics getNodeMetrics() 
	{
		NodeMetrics metrics = new NodeMetrics();
		metrics.setCpuUsage(cpuUsage);
		metrics.setMemoryUsage(memoryUsage);
		metrics.setNodeThroughput(this.nodeAverageThroughput);
		metrics.setNodeResponseTime(this.nodeAverageResponseTime);
		metrics.setNumberOfObjects(this.getNumberOfObjects());
		
		logger.info(String.format("Node %s metrics: CPU %f %%; Mem %f %%;"+ 
				" RT %f ms; TP %f req/s; Objects %d",
			nodeId, metrics.getCpuUsage(), metrics.getMemoryUsage(), 
			metrics.getNodeResponseTime(), metrics.getNodeThroughput(), 
			metrics.getNumberOfObjects() ));
		
		return metrics;
	}

	/* (non-Javadoc)
	 * @see camid.services.nodemanager.INodeManager#migrateObjects(java.lang.String, double)
	 */
	public void migrateObjects(String nodeId, Integer quantity) 
	{
		List<String> objectIds = selectObjectsForMigration(quantity);
		Endpoint ep = this.nameService.getNodeEndpoint(nodeId);
		if(requestor != null)
		{
			ProxyBuilder<INodeManager> builder = new ProxyBuilder<INodeManager>(INodeManager.class, UUID.randomUUID().toString(),
				nodeId, NodeManager.class, requestor, ep);
			INodeManager nm = builder.newProxy();
			for(String objectId : objectIds)
			{
				migrateObject(objectId, nm);
			}
		}
	}

	private List<String> selectObjectsForMigration(Integer quantity) {
		List<String> objectIds = new ArrayList<String>();
		synchronized(statusMap)
		{
			Iterator<String> hostedIds = statusMap.keySet().iterator();
			while( quantity > 0 && hostedIds.hasNext() )
			{
				String hostedId = hostedIds.next();
				objectIds.add(hostedId);
				quantity--;
			}
		}
		return objectIds;
	}

	private Double nodeAverageResponseTime = 0.0;
	
	private Double nodeAverageThroughput = 0.0;
	
	public void setResponseTime(Double averageTime) 
	{
		this.nodeAverageResponseTime = averageTime;
	}

	public void setThroughput(Double averageThroughput) 
	{
		this.nodeAverageThroughput = averageThroughput;
	}

	public Double getNodeAverageResponseTime() 
	{
		return nodeAverageResponseTime;
	}

	public Double getNodeAverageThroughput() 
	{
		return nodeAverageThroughput;
	}

	public void registerAllManagedObjects()
	{
		Reflections r = new Reflections("");
		Set<Class<?>> clsSet = r.getTypesAnnotatedWith(ManagedObject.class, false);
		if(clsSet.size() > 0)
		{
			for(Class<?> cls : clsSet )
			{
				
				this.registerDefinition(cls);
			}
		}
		else
		{
			//class camid.services.nodemanager.NodeManager
			//class camid.test.testobjects.ServerHelloMessage
			//class camid.services.nameservice.NameService
			//class camid.test.testobjects.IntegerBuffer
			//class camid.test.testobjects.Calculator
			//class camid.test.testobjects.DelayObject
			this.registerDefinition(NodeManager.class);
			this.registerDefinition(ServerHelloMessage.class);
			this.registerDefinition(NameService.class);
			this.registerDefinition(IntegerBuffer.class);
			this.registerDefinition(Calculator.class);
			this.registerDefinition(DelayObject.class);
			this.registerDefinition(StatefulDelayObject.class);

		}
	}

	
	
}
